/*
 * NetworkOps.java
 *
 * Created on April 29, 2007, 2:22 AM
 *
 * Copyright 2008 David D. Emory
 * 
 * This file is part of Five Points. See <http://www.fpdev.org> for
 * additional information regarding the project.
 * 
 * Five Points is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Five Points is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Five Points.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.fpdev.apps.rtemaster;

import org.fpdev.apps.rtemaster.shp.ShapefileIO;
import org.fpdev.apps.rtemaster.shp.ShpImportProfile;
import org.fpdev.apps.rtemaster.gui.dialogs.LinkPropertiesDialog;
import com.linuxense.javadbf.DBFReader;
import org.jgrapht.graph.*;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import java.awt.geom.Point2D;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import org.fpdev.core.FPEngine;
import org.fpdev.core.Scenario;
import org.fpdev.core.basenet.BLinkLRT;
import org.fpdev.core.basenet.BLinkRR;
import org.fpdev.core.basenet.BaseNetGraph;
import org.fpdev.core.basenet.BaseNetwork;
import org.fpdev.core.basenet.BLink;
import org.fpdev.core.basenet.BLinkBikePed;
import org.fpdev.core.basenet.BLinkMetro;
import org.fpdev.core.basenet.BLinkStreet;
import org.fpdev.core.basenet.BNode;
import org.fpdev.core.basenet.Path;
import org.fpdev.core.basenet.LinkTypes;
import org.fpdev.core.transit.Route;
import org.fpdev.core.transit.SubRoute;
import org.fpdev.core.transit.TransitPath;
import org.fpdev.apps.rtemaster.gui.dialogs.AddressRangeDialog;
import org.fpdev.apps.rtemaster.gui.dialogs.SelectLinkTypeDialog;
import org.fpdev.util.gui.GUIFactory;
import org.fpdev.apps.rtemaster.actions.ACAction;
import org.fpdev.apps.rtemaster.actions.CreateLinkAction;
import org.fpdev.apps.rtemaster.actions.CreateNodeAction;
import org.fpdev.apps.rtemaster.actions.DeleteLinkAction;
import org.fpdev.apps.rtemaster.actions.DeleteLinksAction;
import org.fpdev.apps.rtemaster.actions.DeleteNodeAction;
import org.fpdev.apps.rtemaster.actions.DeleteNodesAction;
import org.fpdev.apps.rtemaster.actions.MergeNodeAction;
import org.fpdev.apps.rtemaster.actions.MoveNodeAction;
import org.fpdev.apps.rtemaster.actions.RenameLinksAction;
import org.fpdev.apps.rtemaster.actions.SplitLinkAction;
import org.fpdev.apps.rtemaster.actions.EditAddressRangesAction;
import org.fpdev.apps.rtemaster.actions.EditLinksPropertiesAction;
import org.fpdev.apps.rtemaster.actions.EditScenLinkTypesAction;
import org.fpdev.apps.rtemaster.actions.EditShapePointsAction;
import org.fpdev.apps.rtemaster.actions.MoveElementsToScenarioAction;
import org.fpdev.apps.rtemaster.actions.OffsetLinksAction;
import org.fpdev.apps.rtemaster.actions.StitchLinksAtNodeAction;
import org.fpdev.apps.rtemaster.gui.dialogs.ImportStreetsDialog;
import org.fpdev.apps.rtemaster.gui.dialogs.ScenarioLinkTypesDialog;
import org.fpdev.util.FPUtil;
import org.geotools.data.FeatureSource;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.jgrapht.traverse.ClosestFirstIterator;
import org.opengis.feature.simple.SimpleFeature;

/**
 *
 * @author demory
 */
public class NetworkOps {

  private RouteMaster ac_;
  private FPEngine engine_;
  private BaseNetwork baseNet_;
  
  private ShapefileIO shpIO_;
  
  private int maxNodeID_,  maxLinkID_;
  private boolean verbose_;
  private File curDir_;

  /** Creates a new instance of ImportManager */
  public NetworkOps(RouteMaster ac) {
    ac_ = ac;
    engine_ = ac_.getEngine();
    baseNet_ = engine_.getBaseNet();
    shpIO_ = new ShapefileIO(ac_);
    verbose_ = true;

    maxNodeID_ = Math.max(1000000, engine_.getBaseNet().getMaxNodeID());
    maxLinkID_ = Math.max(1000000, engine_.getBaseNet().getMaxLinkID());
  }

  public ShapefileIO getShapefileIO() { return shpIO_; }

  public void setVerbose(boolean verbose) {
    verbose_ = verbose;
  }

  public static boolean isNetworkEvent(RMEvent e) {
    return e.getType() / 100 % 10 == 6; // true if type id = X6XX
  }

  public void handleNetworkEvent(RMEvent e) {

    switch (e.getType()) {


      // Map Click Events

      case EventTypes.MCLICK_MAP_DELETE_NODE:
        userDeleteNode(e.getNodeProperty("node"));
        ac_.getGUI().getMapPanel().refresh(true, true, false);
        break;

      case EventTypes.MCLICK_MAP_DELETE_LINK:
        userDeleteLink(e.getLinkProperty("link"));
        ac_.getGUI().getMapPanel().refresh(true, true, false);
        break;

      case EventTypes.MCLICK_MAP_SPLIT_LINK:
        splitLink(e.getLinkProperty("link"), e.getDoubleProperty("x"), e.getDoubleProperty("y"), true);
        ac_.getGUI().getMapPanel().refresh(true, true, true);
        break;

      case EventTypes.MCLICK_MAP_STITCH_LINKS:
        stitchLinksAtNode(e.getNodeProperty("node"));
        break;

      case EventTypes.MCLICK_MAP_DELETE_SHPPT:
        deleteShapePoint(e.getLinkProperty("link"), e.getDoubleProperty("x"), e.getDoubleProperty("y"));
        break;

      case EventTypes.MCLICK_MAP_ADD_SHPPT:
        addShapePoint(e.getLinkProperty("link"), e.getDoubleProperty("x"), e.getDoubleProperty("y"));
        break;

      // "Map" Menu Events

      case EventTypes.MAPMENU_CLEAR_SELECTED:
        ac_.getGUI().getMapPanel().getSelectedLinks().clear();
        ac_.getGUI().getMapPanel().getSelectedNodes().clear();
        ac_.getGUI().getMapPanel().clearTraceHoverPath();
        ac_.getGUI().getMapPanel().refresh(true, false, false);
        break;
        
      case EventTypes.MAPMENU_DELETE_SELECTED:
        if(JOptionPane.showConfirmDialog(ac_.getGUI(), "This action is not undoable. Proceed?", "Continue?", JOptionPane.YES_NO_OPTION) == JOptionPane.NO_OPTION) break;
        if(!ac_.getGUI().getMapPanel().getSelectedLinks().isEmpty()) deleteSelectedLinks();
        if(!ac_.getGUI().getMapPanel().getSelectedNodes().isEmpty()) deleteSelectedNodes();
        break;

      case EventTypes.MAPMENU_EDIT_LINK_PROPERTIES:
        if(e.getLinkProperty("link") != null)
          editLinkProperties(Collections.singleton(e.getLinkProperty("link")));
        else editLinkProperties();
        break;

      case EventTypes.MAPMENU_EDIT_SCEN_LINK_TYPES:
        if(e.getLinkProperty("link") != null)
          editScenLinkTypes(Collections.singleton(e.getLinkProperty("link")));
        else editScenLinkTypes();
        break;

      case EventTypes.MAPMENU_SIMPLIFY_SHPPTS:
        simplifyShapePoints();
        break;

      case EventTypes.MAPMENU_OFFSET_LINKS:
        createOffsetLinks();
        break;
        
      case EventTypes.MAPMENU_IMPORT_LINKS:
        importLinksFromShapefile();
        break;
        
      case EventTypes.MAPMENU_IMPORT_STREETS_WIZARD:
        importStreetsWizard();
        break;

      case EventTypes.MAPMENU_IMPORT_ELEV_DATA:
        readElevDataFromDBF();
        break;

      case EventTypes.MAPMENU_IMPORT_ZIP_DATA:
        readZIPDataFromDBF();
        break;        
    }


  }

  public void deleteSelectedNodes() {
    Set<BNode> selNodes = ac_.getGUI().getMapPanel().getSelectedNodes();
    for(Iterator<BNode> nodes = selNodes.iterator(); nodes.hasNext();) {
      if(!nodes.next().canDelete(ac_.getEngine().getBaseNet(), ac_.getGUI())) {
        ac_.msg("Delete Selected Nodes Failed");
        return;
      }
    }
    
    ACAction action = new DeleteNodesAction(selNodes);
    action.doAction(ac_);
    ac_.getHistory().addAction(action);
    
    selNodes.clear();
    
    ac_.getGUI().getMapPanel().refresh(true, true, true);
  }
  
  public void deleteSelectedLinks() {
    Set<BLink> selLinks = ac_.getGUI().getMapPanel().getSelectedLinks();
    for(Iterator<BLink> links = selLinks.iterator(); links.hasNext();) {
      if(!links.next().canDelete(ac_.getGUI())) {
        ac_.msg("Delete Selected Links Failed");
        return;
      }
    }
    
    ACAction action = new DeleteLinksAction(selLinks);
    action.doAction(ac_);
    ac_.getHistory().addAction(action);
    
    selLinks.clear();
    
    ac_.getGUI().getMapPanel().refresh(true, true, true);
  }
  
  public void editLinkProperties() {
    if (ac_.getGUI().getMapPanel().getSelectedLinks().size() == 0) {
      ac_.msg("No links currently selected");
      return;
    }
    editLinkProperties(new HashSet<BLink>(ac_.getGUI().getMapPanel().getSelectedLinks()));
  }

  public void editLinkProperties(Set<BLink> links) {
    LinkPropertiesDialog dialog = new LinkPropertiesDialog(links, ac_);
    if (!dialog.okPressed()) {
      return;
    }

    EditLinksPropertiesAction action =
            new EditLinksPropertiesAction(links,
                                          dialog.getNewType(),
                                          dialog.getNewTrnProv(),
                                          dialog.getNewBPProv(),
                                          dialog.getNewName(),
                                          dialog.getDirection(),
                                          dialog.getOneWayOrigin());
    action.doAction(ac_);
    ac_.getHistory().addAction(action);
    ac_.getGUI().getMapPanel().refresh(false, true, false);

  }

  public void editScenLinkTypes() {
    if (ac_.getGUI().getMapPanel().getSelectedLinks().size() == 0) {
      ac_.msg("No links currently selected");
      return;
    }
    editScenLinkTypes(new HashSet<BLink>(ac_.getGUI().getMapPanel().getSelectedLinks()));
  }

  public void editScenLinkTypes(Set<BLink> links) {
    Set<BLinkStreet> streets = new HashSet<BLinkStreet>();
    for(BLink link : links) {
      if(link.getClassType() != BLink.CLASS_STREET) {
        ac_.msg("Scenario-specific type overrides may only be applied to street links");
        return;
      }
      streets.add((BLinkStreet) link);
    }
    ScenarioLinkTypesDialog dialog = new ScenarioLinkTypesDialog(streets, ac_);
    if (!dialog.okPressed()) return;

    EditScenLinkTypesAction action =
            new EditScenLinkTypesAction(streets, dialog.getOverrides());
    action.doAction(ac_);
    ac_.getHistory().addAction(action);
    ac_.getGUI().getMapPanel().refresh(false, true, false);
  }

  public void moveElementsToScenario(Scenario scenario) {
    
    Set<BLink> selLinks = ac_.getGUI().getMapPanel().getSelectedLinks();
    Set<BNode> selNodes = ac_.getGUI().getMapPanel().getSelectedNodes();
    
    if(selLinks.size() == 0 && selNodes.size() == 0) {
      ac_.msg("No links or nodes selected");
      return;
    }
    
    // CHECK FOR ILLEGAL NODE/LINK RELATIONSHIPS RESULTING FROM MOVE
    
    for (BLink link : selLinks) {

      // check for links no longer guaranteed access to one or both nodes
      if(!link.getFNode().getScenario().isAccessibleTo(scenario) && !selNodes.contains(link.getFNode())) {
        ac_.msg("Cannot move elements; node "+link.getFNodeID()+" would no longer be accessible to link "+link.getID());
        return;
      }
      if(!link.getTNode().getScenario().isAccessibleTo(scenario) && !selNodes.contains(link.getTNode())) {
        ac_.msg("Cannot move elements; node "+link.getTNodeID()+" would no longer be accessible to link "+link.getID());
        return;
      }

      // check for routes for which the migrated link would not be visible
      for(Path path : link.getRegisteredPaths()) {
        if(path.isTransitPath()) {
          Route rte = ((TransitPath) path).getSubRoute().getRoute();
          if(!scenario.isAccessibleTo(rte.getScenario())) {
            ac_.msg("Cannot move elements; the scenario association for route \""+rte.getID()+" "+rte.getName()+"\" is not compatible with the proposed move of link "+link.getID());
            return;
          }
        }
      }
    }
    
    // check for nodes that would no longer be accessible to dependent links 
    for (BNode node : selNodes) {
      for(BLink link : ac_.getEngine().getBaseNet().incidentLinks(node)) {
        if(!scenario.isAccessibleTo(link.getScenario()) && !selLinks.contains(link)) {
          ac_.msg("Cannot move elements; node "+node.getID()+" would no longer be accessible to link "+link.getID());
          return;
        }
      }

    }
 
    ACAction action;
    action = new MoveElementsToScenarioAction(new HashSet<BNode>(ac_.getGUI().getMapPanel().getSelectedNodes()),
                                              new HashSet<BLink>(ac_.getGUI().getMapPanel().getSelectedLinks()),
                                              scenario);
    action.doAction(ac_);
    ac_.getHistory().addAction(action);
    ac_.getGUI().getMapPanel().getSelectedLinks().clear();
    ac_.getGUI().getMapPanel().getSelectedNodes().clear();
    ac_.getGUI().getMapPanel().refresh(true, true, false);

  }

  public void simplifyShapePoints() {
    Iterator<BLink> links = ac_.getGUI().getMapPanel().getSelectedLinks().iterator();
    while (links.hasNext()) {
      BLink link = links.next();
      link.simplifyShapePoints();
      ac_.getEngine().getDB().updateShapePoints(link);
    }
    ac_.getGUI().getMapPanel().getSelectedLinks().clear();
    ac_.getGUI().getMapPanel().refresh(false, true, true);
  }

  public void importLinksFromShapefile() {

    List<BLink> processedLinks = new LinkedList<BLink>();
    boolean divideLinks = false;
    verbose_ = false;
    try {
      // get the shapefile URL by either loading it from the file system
      // or from the classpath
      URL shapeURL = null;
      JFileChooser fileChooser = new JFileChooser();
      if(curDir_ != null) fileChooser.setCurrentDirectory(curDir_);
      //fileChooser.setFileFilter(new SimpleFileFilter("shp", "Shapefile"));

      int result = fileChooser.showOpenDialog(null);

      if (result == JFileChooser.APPROVE_OPTION) {
        File f = fileChooser.getSelectedFile();
        curDir_ = fileChooser.getCurrentDirectory();
        shapeURL = f.toURI().toURL();
      }

      if (shapeURL == null) {

        return;
      //shapeURL = new File("c:\\demory\\data\\gis\\aris\\Transit_Marta_Rail.shp").toURI().toURL();
      }

      SelectLinkTypeDialog dialog = new SelectLinkTypeDialog(ac_);
      if (!dialog.okPressed()) {
        return;
      }
      int linkType = dialog.getType();  
      divideLinks = JOptionPane.showConfirmDialog(ac_.getGUI(), "Divide imported links by point elements (e.g. stations)?", "Question", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION;
    
      
      // get feature results
      ShapefileDataStore store = new ShapefileDataStore(shapeURL);
      // feature type name is defaulted to the name of shapefile (without extension)
      String name = store.getTypeNames()[0];
      FeatureSource source = store.getFeatureSource(name);
      FeatureCollection fColl = source.getFeatures();

      int totalItems = fColl.size();
      ac_.msg("Feature collection type: " + fColl.getSchema().getName());
      ac_.msg("Number of features: " + totalItems);

      // READ FEATURES INTO IMPORT GRAPH
      FeatureIterator fIter = fColl.features();

      ac_.getGUI().statusText("Importing features to graph...");
      ImportGraph iLinks = new ImportGraph();
      int numProcessed = 0;
      
      while (fIter.hasNext()) {
        
        SimpleFeature f = (SimpleFeature) fIter.next();
        Geometry g = (Geometry) f.getDefaultGeometry();
        Coordinate[] coords = g.getCoordinates();
        iLinks.addLink(new ImportGraph.Link(coords, f));
        numProcessed++;
        double pctWrittenD = 100.0 * (double) numProcessed / (double) totalItems;
        ac_.setProgress((int) pctWrittenD);
        
      }
      ac_.setProgress(0);
        
      //cv_.getGUI().getMapPanel().getDrawItems().addItem("impshp", iLinks, "m");
      ac_.getGUI().getMapPanel().refresh(false, true, false);

      // check node concurrency
      double tol = 10;
      SimpleGraph<ImportGraph.Node, DefaultEdge> proxGraph =
              new SimpleGraph<ImportGraph.Node, DefaultEdge>(DefaultEdge.class);
      Map<Coordinate, BNode> coordLookup = new HashMap<Coordinate, BNode>();

      List<Coordinate> endPts = iLinks.endPoints();
      int epCount = endPts.size();
      ImportGraph.Node iNodes[] = new ImportGraph.Node[epCount];

      ac_.getGUI().statusText("Initializing proximity graph vertices...");
      totalItems = epCount; 
      Iterator<Coordinate> epIter = endPts.iterator();
      int i1 = 0;
      while (epIter.hasNext()) {
        Coordinate c1 = epIter.next();
        ImportGraph.Node iNode = new ImportGraph.Node("" + i1, c1);
        proxGraph.addVertex(iNode);
        iNodes[i1] = iNode;
        i1++;
        double pctWrittenD = 100.0 * (double) i1 / (double) totalItems;
        ac_.setProgress((int) pctWrittenD);
      }
      ac_.setProgress(0);
      

      ac_.getGUI().statusText("Computing proximity graph distances...");
      totalItems = epCount*epCount/2;
      numProcessed = 0;
      Coordinate[] coords = new Coordinate[endPts.size()];
      int i = 0;
      for(epIter = endPts.iterator(); epIter.hasNext();) coords[i++] = epIter.next(); 
      for (i1 = 0; i1 < coords.length; i1++) {
        for (int i2 = i1+1; i2 < coords.length; i2++) {
          double dist = FPUtil.magnitude(coords[i1].x, coords[i1].y, coords[i2].x, coords[i2].y);
          if (dist <= tol) { // create link
            proxGraph.addEdge(iNodes[i1], iNodes[i2]);
          }        
          numProcessed++;
          double pctWrittenD = 100.0 * (double) numProcessed / (double) totalItems;
          ac_.setProgress((int) pctWrittenD);
        }       
      }
      ac_.setProgress(0);
      /*epIter = endPts.iterator();
      i1 = 0;
      int e = 1;
      while (epIter.hasNext()) {
        Coordinate c1 = epIter.next();
        //System.out.println("coord "+c1);

        Iterator<Coordinate> compIter = endPts.iterator();
        int i2 = 0;
        while (compIter.hasNext()) {
          Coordinate c2 = compIter.next();
          if (i1 != i2) {
            double dist = FPUtil.magnitude(c1.x, c1.y, c2.x, c2.y);
            //System.out.println("  comp to "+c2+", dist="+dist);
            if (dist <= tol) { // create link
              proxGraph.addEdge(iNodes[i1], iNodes[i2]);
              e++;
            }
          }
          i2++;
        }
        i1++;
        double pctWrittenD = 100.0 * (double) i1 / (double) totalItems;
        av_.setProgress((int) pctWrittenD);
      }
      av_.setProgress(0);*/

      System.gc();
      
      ac_.getGUI().statusText("Reducing proximity graph and creating nodes in db...");
      Set<ImportGraph.Node> verts = new HashSet<ImportGraph.Node>();
      verts.addAll(proxGraph.vertexSet());
      totalItems = verts.size();
      engine_.getDB().setActiveTable("nodes");
      while (!verts.isEmpty()) {//vIter.hasNext()) {
        ImportGraph.Node iNode = verts.iterator().next(); //vIter.next();
        if (proxGraph.degreeOf(iNode) == 0) { // freestanding node
          //System.out.println("fs node "+iNode.getID());
          verts.remove(iNode);
          BNode node = newNode(iNode.getCoord().x, iNode.getCoord().y, false).getNode();
          engine_.getDB().initNode(node);
          coordLookup.put(iNode.getCoord(), node);
        } else {
          double tx = 0, ty = 0, count = 1;
          //System.out.println("grouped node "+iNode.getID());
          tx += iNode.getCoord().x;
          ty += iNode.getCoord().y;
          ClosestFirstIterator cfi = new ClosestFirstIterator(proxGraph, iNode);
          while (cfi.hasNext()) {
            ImportGraph.Node in = (ImportGraph.Node) cfi.next();
            //System.out.println(" in: "+in.getID());
            verts.remove(in);
            tx += in.getCoord().x;
            ty += in.getCoord().y;
            count++;
          }
          BNode node = newNode(tx / count, ty / count, false).getNode();
          engine_.getDB().initNode(node);

          // re-iterate, updating coord lookup table
          cfi = new ClosestFirstIterator(proxGraph, iNode);
          while (cfi.hasNext()) {
            coordLookup.put(((ImportGraph.Node) cfi.next()).getCoord(), node);
          }

        }
        double pctWrittenD = 100.0 * (double) (totalItems - verts.size()) / (double) totalItems;
        ac_.setProgress((int) pctWrittenD);

      }
      engine_.getDB().flushInsertOps();
      ac_.setProgress(0);

      System.gc();
      
      // initialize edges
      ac_.getGUI().statusText("Creating edges in db...");
      totalItems = iLinks.linkCount(); 
      numProcessed = 0;
      engine_.getDB().setActiveTable("links");
      Iterator<ImportGraph.Link> links = iLinks.links();
      while (links.hasNext()) {
        ImportGraph.Link link = links.next();
        //System.out.println(link);
        CreateLinkAction cla = newLink(null, coordLookup.get(link.start()), coordLookup.get(link.end()), linkType, link.shapePoints(), 0, false);
        if (cla != null && cla.getLink() != null) {
          engine_.getDB().initLink(cla.getLink());
          if(divideLinks) processedLinks.add(cla.getLink());
        }
        numProcessed++;
        double pctWrittenD = 100.0 * (double) numProcessed / (double) totalItems;
        ac_.setProgress((int) pctWrittenD);
        
      }
      engine_.getDB().flushInsertOps();      
      ac_.setProgress(0);


    } catch (Exception e) {
      System.out.println("Exception in importShapefile()");
      e.printStackTrace();
    }

    if(divideLinks) divideLinksByPoints(processedLinks);
    verbose_ = true;

  }

  public void importStreetsWizard() {
    importStreetsWizard(null);
  }
  
  public void importStreetsWizard(String initFile) {
    ImportStreetsDialog isd = new ImportStreetsDialog(ac_, initFile);
    System.out.println("impPressed="+isd.importPressed());
    if(!isd.importPressed()) return;
    ShpImportProfile profile = isd.getCurrentProfile();
    System.out.println("profile="+profile);
    if(profile == null) return;
    shpIO_.getImporter().importFromShapefile2(profile, isd.getFileURL());
  }

  public void divideLinksByPoints(List<BLink> links) {
    try {
      URL shapeURL = null;
      JFileChooser fileChooser = new JFileChooser();
      //fileChooser.setFileFilter(new SimpleFileFilter("shp", "Shapefile"));

      int result = fileChooser.showOpenDialog(null);

      if (result == JFileChooser.APPROVE_OPTION) {
        File f = fileChooser.getSelectedFile();
        shapeURL = f.toURL();
      }

      if (shapeURL == null) {
        shapeURL = new File("c:\\demory\\data\\gis\\aris\\Transit_Marta_Stations.shp").toURL();
      }

      // get feature results
      ShapefileDataStore store = new ShapefileDataStore(shapeURL);
      // feature type name is defaulted to the name of shapefile (without extension)
      String name = store.getTypeNames()[0];
      FeatureSource source = store.getFeatureSource(name);
      FeatureCollection fColl = source.getFeatures();

      System.out.println("Feature collection type: " + fColl.getSchema().getName());
      System.out.println("Num features: " + fColl.size());

      FeatureIterator fIter = fColl.features();

      double tol = 200;
      while (fIter.hasNext()) {
        int splitLinks = 0;
        SimpleFeature f = (SimpleFeature) fIter.next();
        System.out.println("Station: " + f.getAttribute(1));
        Geometry g = (Geometry) f.getDefaultGeometry();
        Coordinate[] coords = g.getCoordinates();
        //System.out.println("coords len="+coords.length);
        if (coords.length == 1) {
          List<BLink> listCopy = new LinkedList<BLink>(links);
          Iterator<BLink> linkIter = listCopy.iterator();
          int i = 0;
          while (linkIter.hasNext()) {
            BLink link = linkIter.next();
            double x = coords[0].x, y = coords[0].y;
            double distToStart = FPUtil.magnitude(x, y, link.getX1(), link.getY1());
            double distToEnd = FPUtil.magnitude(x, y, link.getX2(), link.getY2());
            double distToLink = link.distToPoint(x, y);
            //System.out.println(" DTS="+distToStart+"  DTE="+distToEnd+"  DTL="+distToLink);
            if (distToStart > tol && distToEnd > tol && distToLink < tol) {
              //System.out.println(" splitting link "+i);
              SplitLinkAction sla = splitLink(link, x, y, false);
              if(sla != null) {
                splitLinks++;
                links.remove(link);
                links.add(sla.getSplitLink1());
                links.add(sla.getSplitLink2());
              }
            }

          }
          i++;
        }
        if (splitLinks > 0) {
          System.out.println("Split " + splitLinks + " for " + f.getAttribute(1));
        } else {
          System.out.println("No split links for " + f.getAttribute(1));
        }

      }

    } catch (Exception ex) {
      ex.printStackTrace();
    }
  }

  //// CREATE/DELETE NODE METHODS

  public int newNodeID() {
    return ++maxNodeID_;
  }

  public void userNewNode(double wx, double wy) {
    CreateNodeAction action = newNode(wx, wy, true);
    if (action != null) {
      ac_.getHistory().addAction(action);
    }
  }

  public CreateNodeAction newNode(double wx, double wy) {
    return newNode(wx, wy, true);
  }

  public CreateNodeAction newNode(double wx, double wy, boolean updateDB) {
    return newNode(newNodeID(), ac_.getActiveScenario(), wx, wy, updateDB);
  }

  public CreateNodeAction newNode(int id, double wx, double wy, boolean updateDB) {
    return newNode(id, ac_.getActiveScenario(), wx, wy, updateDB);
  }

  public CreateNodeAction newNode(int id, Scenario scen, double wx, double wy, boolean updateDB) {
    if(verbose_) ac_.msg("New node with ID " + id);

    BNode node = new BNode(id, scen, wx, wy);
    CreateNodeAction action = new CreateNodeAction(node, updateDB);
    action.doAction(ac_);
    return action;
  }

  public void userDeleteNode(BNode node) {
    DeleteNodeAction action = deleteNode(node, true);
    if (action != null) ac_.getHistory().addAction(action);
  }

  public DeleteNodeAction deleteNode(BNode node, boolean updateDB) {
    DeleteNodeAction action = new DeleteNodeAction(node, updateDB);
    boolean success = action.doAction(ac_);

    return success ? action : null;
  }

  public void mergeNode(BNode node1, BNode node2) {
    if (node1 == node2) {
      ac_.msg("Nodes are coincident");
      return;
    }
    ac_.msg("Merging nodes");

    Iterator<BLink> iLinks = baseNet_.incidentLinks(node1).iterator();

    // check for stop at merging node

    engine_.initTransitAnnotations();
    while (iLinks.hasNext()) {
      BLink link = iLinks.next();
      Iterator<SubRoute> subs = link.getAllSubRoutes();
      while (subs.hasNext()) {
        SubRoute sub = subs.next();
        if (sub.getPath().hasStop(node1)) {
          ac_.msg("Merging node contains stop");
          return;
        }

      }
    }



    MergeNodeAction action = new MergeNodeAction(node1.getID(), node2.getID());
    action.doAction(ac_);
    ac_.getHistory().addAction(action);

    engine_.clearTransitAnnotations();
    
  }

  public void movingNode(BNode node, double wx, double wy) {
    node.setXY(wx, wy);
    Iterator<BLink> links = engine_.getBaseNet().incidentLinks(node).iterator();
    while (links.hasNext()) {
      BLink link = links.next();
      link.updateXY();
    }
  }
  private Point2D.Double oldNodeLoc_;

  public void startMovingNode(BNode node) {
    oldNodeLoc_ = new Point2D.Double(node.getX(), node.getY());
  }

  public void finishedMovingNode(BNode node) {
    /*Iterator<CLink> links = engine_.getBaseNet().incidentLinks(node).iterator();
    while(links.hasNext()) {
    CLink link = links.next();
    link.updateBBXY();
    }*/
    ACAction action = new MoveNodeAction(node, oldNodeLoc_, new Point2D.Double(node.getX(), node.getY())); //, engine_.getBaseNet().incidentLinks(node));
    action.doAction(ac_);
    ac_.getHistory().addAction(action);

  //action.updateDB(engine_.getDB());    
  }

  //// CREATE/DELETE LINK METHODS

  public int newLinkID() {
    return ++maxLinkID_;
  }

  public void userNewLink(BNode start, BNode end) {
    Scenario as = ac_.getActiveScenario();
    if (start.getScenario() != as && !start.getScenario().isAncestorOf(as)) {
      ac_.msg("Could not create link; selected scenario not compatible with start node");
      return;
    }
    if (end.getScenario() != as && !end.getScenario().isAncestorOf(as)) {
      ac_.msg("Could not create link; selected scenario not compatible with end node");
      return;
    }

    CreateLinkAction action;
    action = newLink(newLinkID(), null, start, end, ac_.getGUI().getLinkOptionsPanel().getSelectedType(), null, 0, true);
    if (action != null) {
      ac_.getHistory().addAction(action);
    }
  }

  public CreateLinkAction newLink(Scenario scen, BNode start, BNode end, int type, List<Point2D.Double> shpPts, double lenFt, boolean updateDB) {
    return newLink(0, scen, start, end, type, shpPts, lenFt, updateDB);
  }

  public CreateLinkAction newLink(int id, Scenario scen, BNode start, BNode end, int type, List<Point2D.Double> shpPts, double lenFt, boolean updateDB) {
    String name = ac_.getGUI().getLinkOptionsPanel().getName();
    return newLink(id, name, scen, start, end, type, shpPts, lenFt, updateDB);
  }      
  
  public CreateLinkAction newLink(int id, String linkName, Scenario scen, BNode start, BNode end, int type, List<Point2D.Double> shpPts, double lenFt, boolean updateDB) {
    //String linkName = av_.getGUI().getLinkOptionsPanel().getName();
    int lenFt_i = (int) lenFt;
    if (ac_.getGUI().getLinkOptionsPanel().getConfirmLength()) {
      String lenFtStr = JOptionPane.showInputDialog("Please specify a length (ft):", "" + lenFt);
      lenFt_i = (int) (new Double(lenFtStr)).doubleValue();
    }
    if (id == 0) {
      id = newLinkID();
    }
    if (type == 0) {
      type = ac_.getGUI().getLinkOptionsPanel().getSelectedType();
    }
    if (scen == null) {
      scen = ac_.getActiveScenario();
    }
    BLink link = null;

    switch (LinkTypes.linkTypetoClassType(type)) {
      case BLink.CLASS_STREET:
        if(verbose_) ac_.msg("New street link with ID " + id);

        link = new BLinkStreet(scen, id, type, start, end, linkName, lenFt_i);
        //((CLinkStreet) link).setWalkable(av_.getGUI().getLinkOptionsPanel().getIsWalkable());
        break;
      case BLink.CLASS_BIKEPED:
        if(verbose_) ac_.msg("New bikeped link with ID " + id);
        link = new BLinkBikePed(scen, id, start, end, linkName, lenFt_i);
        break;
      case BLink.CLASS_METRO:
        if(verbose_) ac_.msg("New HRT link with ID " + id);
        link = new BLinkMetro(scen, id, start, end, linkName, lenFt_i);
        break;
      case BLink.CLASS_LRT:
        if(verbose_) ac_.msg("New LRT link with ID " + id);
        link = new BLinkLRT(scen, id, start, end, linkName, lenFt_i);
        break;
      case BLink.CLASS_RAILROAD:
        if(verbose_) ac_.msg("New RR link with ID " + id);
        link = new BLinkRR(scen, id, start, end, linkName, lenFt_i);
        break;
    }
    if (link != null) {
      if (lenFt == 0) {
        link.setLengthFeet(link.getMeasuredLength());
      }
      if (shpPts != null && !shpPts.isEmpty()) {
        link.setShapePoints(shpPts);
      }

      CreateLinkAction action = new CreateLinkAction(link, updateDB);
      action.doAction(ac_);
      return action;
    }

    return null;
  }

  public void userDeleteLink(BLink link) {
    if (link.registeredPathCount() > 0) {
      ac_.msg("Delete failed: link associated with active path(s)");
      return;
    }
    DeleteLinkAction action = deleteLink(link, true);
    if (action != null) ac_.getHistory().addAction(action);
    else ac_.msg("Error deleting link");
  }

  public DeleteLinkAction deleteLink(BLink link, boolean updateDB) {

    DeleteLinkAction action = new DeleteLinkAction(link, updateDB);
    boolean success = action.doAction(ac_);
    return success ? action : null;
  }

  //// OFFSET LINKS METHODS

  public void createOffsetLinks() {
    BaseNetGraph graph = new BaseNetGraph(ac_.getGUI().getMapPanel().getSelectedLinks().iterator());
    if (!graph.isSimplePath()) {
      ac_.msg("Selected links must form continuous linear path");
      return;
    }

    List<String> endPtNames = new LinkedList<String>();
    List<BNode> endPts = graph.getEndPoints();
    Iterator<BNode> ptIter = endPts.iterator();
    while (ptIter.hasNext()) {
      endPtNames.add(ptIter.next().getDisplayName(baseNet_));
    }
    int selected = GUIFactory.showSelectItemDialog(ac_.getGUI(), endPtNames.iterator());
    if (selected == -1) {
      return;
    }


    String dist = JOptionPane.showInputDialog("Offset distance (ft):", "100");
    if (!FPUtil.isInteger(dist)) {
      ac_.msg("Not a valid integer");
      return;
    }
    double distd = new Double(dist).doubleValue();

    ACAction action = new OffsetLinksAction(graph, endPts.get(selected), distd);
    if(action.doAction(ac_)) {
      ac_.getHistory().addAction(action);
      ac_.getGUI().getMapPanel().getSelectedLinks().clear();
      ac_.getGUI().getMapPanel().refresh(true, true, true);
    }
  }

  //// MODIFY LINK METHODS 

  public SplitLinkAction splitLink(BLink link, double mx, double my, boolean addToUndoStack) {

    SplitLinkAction action = new SplitLinkAction(link, mx, my, ac_.getEngine());
    if(action.doAction(ac_)) {
      if(addToUndoStack) ac_.getHistory().addAction(action);
      return action;
    }
    return null;
  }

  public void renameSelectedLinks(String newName) {
    if (newName.length() == 0) {
      ac_.msg("Invalid Link Name");
      return;
    }
    //String newName = JOptionPane.showInputDialog("Please specify the new name:");
		/*Iterator<CLink> links =  getSelectedLinks();
    List<CLink> linkList = new LinkedList<CLink>();
    while(links.hasNext()) {
    CLink link = links.next();
    link.setName(newName);
    linkList.add(link);
    }
    av_.getGUI().getMapPanel().updated(true, false);
    av_.getGUI().getMapPanel().repaint();*/

    ACAction action = new RenameLinksAction(ac_.getGUI().getMapPanel().getSelectedLinks(), newName);
    action.doAction(ac_);
    ac_.getHistory().addAction(action);
  }

  public void setSelectedLinkAddr() {
    if (ac_.getGUI().getMapPanel().getSelectedLinks().size() == 1) {
      BLink link = ac_.getGUI().getMapPanel().getSelectedLinks().iterator().next();
      if (link.getClassType() == BLink.CLASS_STREET) {
        System.out.println("creating ar dialog");
        new AddressRangeDialog(ac_, (BLinkStreet) link).setVisible(true);
      }
    } else {
      ac_.msg("A single link must be selected");
    }
  }

  public void fixSelectedLinkAddr() {
    Iterator<BLink> links = ac_.getGUI().getMapPanel().getSelectedLinks().iterator();
    while (links.hasNext()) {
      BLink link = links.next();
      if (link.getClassType() == BLink.CLASS_STREET) {
        ((BLinkStreet) link).fixAddressRange(baseNet_);
      }
    }
  }

  public void updateLinkAddressRange(BLinkStreet link, int fAddrL, int fAddrR, int tAddrL, int tAddrR, int zipL, int zipR) {
    //link.setAddressRange(fAddrL, fAddrR, tAddrL, tAddrR); 
    ACAction action = new EditAddressRangesAction(link, fAddrL, fAddrR, tAddrL, tAddrR, zipL, zipR);
    action.doAction(ac_);
    ac_.getHistory().addAction(action);
  //action.updateDB(engine_.getDB());
  }

  // SHAPEPOINT METHODS

  private List<Point2D.Double> oldShpPts_;
  private Point2D.Double shpPtLoc_;


  public void deleteShapePoint(BLink link, double x, double y) {
    if (link == null) return;

    oldShpPts_ = new LinkedList<Point2D.Double>(link.getShapePointsListCopy());

    int shpPtIndex = link.getShapePointIndexFromXY(x, y, ac_.getGUI().getMapPanel().getClickToleranceMapUnits());
    if (shpPtIndex >= 0) {
      System.out.println("link " + link.getID() + " shppt " + shpPtIndex);
      link.deleteShapePoint(shpPtIndex);
      ac_.getGUI().getMapPanel().refresh(false, true, true);

      ACAction action = new EditShapePointsAction(link, oldShpPts_);
      if(action.doAction(ac_)) ac_.getHistory().addAction(action);
      //ac_.getEngine().getDB().updateShapePoints(link);
    }

  }

  public void addShapePoint(BLink link, double mx, double my) {

    oldShpPts_ = new LinkedList<Point2D.Double>(link.getShapePointsListCopy());

    double u = 0, ix = 0, iy = 0;
    List<Point2D.Double> shpPts1 = new LinkedList<Point2D.Double>();
    List<Point2D.Double> shpPts2 = new LinkedList<Point2D.Double>();

    int iseg = 0; // index of current internal segment

    if (!link.getShapePoints().hasNext()) { // link with no shapepoints
      double x1 = link.getX1(), y1 = link.getY1(), x2 = link.getX2(), y2 = link.getY2();
      double lineMag = FPUtil.magnitude(x1, y1, x2, y2);
      u = ((mx - x1) * (x2 - x1) + (my - y1) * (y2 - y1)) / (lineMag * lineMag);
      System.out.println("u=" + u);
      if (u > Double.MIN_VALUE && u <= 1) {
        ix = x1 + u * (x2 - x1);
        iy = y1 + u * (y2 - y1);
      } else {
        ac_.msg("Invalid split coordinates");
        return;
      }
    } else { // link with >0 shapepoints
      List<Point2D.Double> points = new LinkedList<Point2D.Double>();
      Iterator<Point2D.Double> shpPts = link.getShapePoints();
      while (shpPts.hasNext()) {
        points.add(shpPts.next());
      }
      points.add(new Point2D.Double(link.getX2(), link.getY2()));
      double x1 = link.getX1(), y1 = link.getY1(), x2, y2;

      Iterator<Point2D.Double> ptIter = points.iterator();
      boolean success = false;
      while (ptIter.hasNext()) {
        Point2D.Double pt = ptIter.next();
        x2 = pt.getX();
        y2 = pt.getY();

        // calculate u for this segment
        double lineMag = FPUtil.magnitude(x1, y1, x2, y2);
        double useg = ((mx - x1) * (x2 - x1) + (my - y1) * (y2 - y1)) / (lineMag * lineMag);
        //System.out.println("u="+useg);

        ix = x1 + useg * (x2 - x1);
        iy = y1 + useg * (y2 - y1);

        double distToSeg = FPUtil.magnitude(ix, iy, mx, my);

        if (useg > Double.MIN_VALUE && useg <= 1 && distToSeg < ac_.getGUI().getMapPanel().getClickToleranceMapUnits()) {
          u += (lineMag / link.getMeasuredLength()) * useg;
          System.out.println("Found @ iseg=" + iseg + " u=" + u);

          // populate 2nd shapepoint list
          shpPts2.add(pt);
          for (int i = iseg; i < points.size() - 1; i++) {
            shpPts2.add(ptIter.next());
          }

          success = true;
          break;
        } else {
          shpPts1.add(pt); // add to 1st shapepoint list
          u += (lineMag / link.getMeasuredLength());
        }

        x1 = x2;
        y1 = y2;
        iseg++;
      }
      if (!success) {
        ac_.msg("cannot add shapepoint");
        return;
      }
    }

    link.addShapePoint(iseg, ix, iy);

    // update database
    //av_.getEngine().getDB().updateShapePoints(link);
    //ac_.getEngine().getDataPackage().getDataConn().modifyLink(link, new LinkAttr[] { LinkAttr.SHAPE });

    ACAction action = new EditShapePointsAction(link, oldShpPts_);
    if(action.doAction(ac_)) ac_.getHistory().addAction(action);
    ac_.getGUI().getMapPanel().refresh(false, true, true);

  }

  public void startMovingShpPt(BLink link, int index) {
    oldShpPts_ = new LinkedList<Point2D.Double>(link.getShapePointsListCopy());
    shpPtLoc_ = link.getShapePointsList().get(index);
  }

  public void movingShpPt(double wx, double wy) { // CLink link, int index,
    shpPtLoc_.x = wx;
    shpPtLoc_.y = wy;
  }

  public void finishedMovingShpPt(BLink link) {

    ACAction action = new EditShapePointsAction(link, oldShpPts_);
    if(action.doAction(ac_)) ac_.getHistory().addAction(action);
    //link.setLengthFeet(link.getMeasuredLength());
    //ac_.getEngine().getDataPackage().getDataConn().modifyLink(link, new LinkAttr[] { LinkAttr.SHAPE, LinkAttr.LEN });

  }

  public void readElevDataFromDBF() {
    try {
      JFileChooser fileChooser = new JFileChooser();
      //fileChooser.setFileFilter(new SimpleFileFilter("shp", "Shapefile"));

      int result = fileChooser.showOpenDialog(null);

      File file = null;
      if (result == JFileChooser.APPROVE_OPTION) {
        file = fileChooser.getSelectedFile();
      }

      if (file == null) {
        return;
      }

      InputStream inputStream = new FileInputStream(file);// (filename);
      DBFReader reader = new DBFReader(inputStream);


      Object[] rowObjects;
      int i = 0;
      while ((rowObjects = reader.nextRecord()) != null) {
        int id = ((Double) rowObjects[0]).intValue();
        double newlen = ((Double) rowObjects[5]).doubleValue(); // 2
        String elev = rowObjects[2].toString(); // 3
        float upfw = ((Float) rowObjects[3]).floatValue(); // 4
        float upbw = ((Float) rowObjects[4]).floatValue(); // 5
				/*if(i % 1000 == 0) {
        System.out.println(id+" "+upfw+" "+upbw+" "+newlen);
        System.out.println(top);
        System.out.println("");
        }*/
        BLink link = ac_.getEngine().getLinkFromID(id);
        if (link != null) {
          link.setLengthFeet(newlen);
          link.setUpSlopeFW(upfw);
          link.setUpSlopeBW(upbw);
          link.setElevStr(elev);
        } else {
          System.out.println("invalid link id: " + id);
        }
        i++;
        if (i % 1000 == 0) {
          System.out.println("processed " + i + " links");
        }
      }
      inputStream.close();
      System.out.println("processed " + i + " links total");

    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public void readZIPDataFromDBF() {
    try {
      /*JFileChooser fileChooser = new JFileChooser();
      int result = fileChooser.showOpenDialog(null);
      File file = null;
      if (result == JFileChooser.APPROVE_OPTION) {
      file = fileChooser.getSelectedFile();
      }
      if(file == null) return;*/

      InputStream inputStream = new FileInputStream("c:\\demory\\5pdev\\ziplookup.dbf"); // (file);
      DBFReader reader = new DBFReader(inputStream);


      Object[] rowObjects;
      int i = 0, s = 0;
      while ((rowObjects = reader.nextRecord()) != null) {
        int id = ((Double) rowObjects[0]).intValue();
        int zipR = ((Double) rowObjects[19]).intValue();
        int zipL = ((Double) rowObjects[20]).intValue();
        BLink link = ac_.getEngine().getLinkFromID(id);
        if (link != null && link.getClassType() == BLink.CLASS_STREET) {
          ((BLinkStreet) link).setZips(zipL, zipR);
          s++;
        }
        //else System.out.println("invalid link id: "+id);
        i++;
        if (i % 1000 == 0) {
          System.out.println("processed " + i + " rows");
        }
      }
      inputStream.close();
      System.out.println("processed " + i + " rows total, success with " + s);

    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  // OTHER LINK/NODE MODIFICATIONS

  public void stitchLinksAtNode(BNode node) {
    System.out.println("NetOps::stitchLinksAtNode");
    StitchLinksAtNodeAction action = new StitchLinksAtNodeAction(node);
    if (action.doAction(ac_)) {
      ac_.getHistory().addAction(action);
      ac_.getGUI().getMapPanel().refresh(true, true, true);
    }
    else ac_.msg("Error stitching links at node");

  }

  void setMaxIDs(int maxNodeID, int maxLinkID) {
    maxNodeID_ = maxNodeID;
    maxLinkID_ = maxLinkID;
    System.out.println("max node id: " + maxNodeID_);
    System.out.println("max link id: " + maxLinkID_);
  }

  private SimpleFeature getDefaultGeometry() {
    throw new UnsupportedOperationException("Not yet implemented");
  }
}
